{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 时钟app"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "一个仿iOS和Android内置时钟应用的app。分两部分：\n",
    "1. 个没有交互的数字时钟，简述Kivy的事件驱动(event-driven)方法，引入计时器的功能，持续更新。\n",
    "2. 交互的秒表功能，设计流畅的自适应布局。\n",
    "\n",
    "<!-- TEASER_END-->"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "学习大纲：\n",
    "* Kivy语言基础，DSL(domain-specific language)处理部件(widgets)\n",
    "* Kivy布局方式\n",
    "* 自定义字体和文字样式\n",
    "* 事件管理\n",
    "\n",
    "app最终效果如下，只要60行代码，Python代码和kv代码各一半。\n",
    "\n",
    "![clockapp](kbpic/1.1clockapp.png \"clockapp\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##起点"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "将kivy的`helloworld`稍作修改。增加一个布局容器(layout container)，`BoxLayout`，后面可增加更多部件。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "# %load ../0_Hello/main.py\n",
    "from kivy.app import App\n",
    "\n",
    "\n",
    "class ClockApp(App):\n",
    "    pass\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    ClockApp().run()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "```yaml\n",
    "# clock.kv\n",
    "BoxLayout:\n",
    "    orientation: 'vertical'\n",
    "\n",
    "    Label:\n",
    "        text: '00:00:00'\n",
    "```            "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`BoxLayout`容器可以包含多个子部件，水平或垂直堆放。由于`kv`只有一个子部件，`BoxLayout`就会让它充满所有空间。\n",
    "> 当运行`main.py`文件时，Kivy自动调用`clock.kv`。类名是`ClockApp`，`.kv`文件名就是`clock`，类名小写并去掉`App`。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##新UI"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "扁平化设计模式(flat design paradigm)如日中天，覆盖Web，移动，桌面应用领域，兴起于iOS7和Win8。互联网公司也追随，于Google I/O 2014出Material design，其他HTML5框架，如Bootstrap亦如是。\n",
    "\n",
    "扁平化设计强调内容胜于外观，忽略逼真图片的阴影和细致的质地，支持纯色和简单几何图形。强调比学院派的仿真设计（skeuomorphic design）更简单的程序化创造，前者倾向于丰富视觉效果和艺术感。\n",
    "\n",
    ">仿真主义是用户界面设计的主流方法。认为应用程序属于真实世界的一部分，比如一个带按钮的计算器app应该被做成廉价的、物质的计算器的感觉，有助于提升用户体验（得看是谁用）。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "如今，放弃视觉细节而转向简单、流线型界面仿佛是共识。另一方面，仅靠一堆彩色框框就想做成惊世骇俗的作品很有难度。扁平化设计成了文字排版好的代名词原因就是文字成了UI设计中重要的部分，所有我们要让文字好看。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###设计灵感"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "模仿Android 4.1 Jelly Bean的时钟设计。字体是Google的[Roboto](http://www.fontsquirrel.com/fonts/roboto)字体，取代了Android 4.0 Ice Cream Sandwich的Droid字体。\n",
    "\n",
    "![clockui](kbpic/1.2android4.1clockUI.png \"clockui\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###加载自定义字体"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Kivy默认是Droid Sans字体，通过`font_name`属性可设置自定义字体。这里只有一种字体，可以直接将`.ttf`文件名放上。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```yaml\n",
    "# clock.kv\n",
    "Label:\n",
    "    font_name: 'Loster.ttf'\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "但是我们要好几种字体，一个属性就不够了。因为不同字体都是单个文件，而属性只能跟一个文件名。涉及多种字体可以用`LabelBase.register`方法可以接受多种字体，如下所示："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "LabelBase.register(name=\"Roboto\",\n",
    "    fn_regular=\"Roboto-Regular.ttf\",\n",
    "    fn_bold=\"Roboto-Bold.ttf\",\n",
    "    fn_italic=\"Roboto-Italic.ttf\",\n",
    "    fn_bolditalic=\"Roboto-BoldItalic.ttf\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "改进之后，一个部件的`font_name`属性可设置多种自定义字体了。但这种方法有两个限制：\n",
    "1. kivy只接受TrueType的`.ttf`字体。如果是OpenType的`.otf`或者网页字体如`.woff`，得先[转换](http://fontforge.org/)。\n",
    "2. 字体normal，italic，bold，bold italic四种样式有最大值。旧字体没问题，如Droid Sans。但是新字体都有4到20多种样式，其高度和其他特征也不同。Roboto至少有12种样式。\n",
    "\n",
    "第二点迫使我们选择app字体时要把12种样式全放进去，这么做会增大app的体积，Roboto字体有1.7M。\n",
    "\n",
    "本例中我们只要两种样式：浅色(`Roboto-Thin.ttf`)和加粗(`Roboto-Medium.ttf`)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "from kivy.core.text import LabelBase\n",
    "\n",
    "LabelBase.register(name='Roboto',\n",
    "    fn_regular='Roboto-Thin.ttf',\n",
    "    fn_bold='Roboto-Medium.ttf')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "下面我们来使用字体，放到`Label`后面即可。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```yaml\n",
    "# clock.kv\n",
    "Label:\n",
    "    text: '00:00:00'\n",
    "    font_name: 'Roboto'\n",
    "    font_size: 60\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###字体格式"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "markup语言毋庸置疑HTML。Kivy实现了另外一种BBCode的markup语言，用[]作标签。\n",
    "\n",
    "| BBCode tag    |Effect on text |\n",
    "| :-------------: |:-------------:|\n",
    "| [b]...[/b] | **Effect on text**|\n",
    "| [i]...[/i] | *Italic*|\n",
    "| [font=Lobster]...[/font] | Change font|\n",
    "| [color=#FF0000]...[/color] | Set color with CSS-like syntax|\n",
    "| [sub]...[/sub] | Subscript (text below the line)|\n",
    "| [sup]...[/sup] | Superscript (text above the line)|\n",
    "| [ref=name]...[/ref] | Clickable zone, `<a href=\"...\">` in HTML|\n",
    "| [anchor=name] | Named location, `<a name=\"...\">` in HTML|\n",
    "\n",
    ">由于Kivy发展很快，以上内容绝非最终版本，详情查阅[kivy文档](http://kivy.org)。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "再看看图2，我们要实现小时数字加粗的效果就easy了。\n",
    "\n",
    "```yaml\n",
    "# clock.kv\n",
    "Label:\n",
    "    text: '[b]00[/b]:00:00'\n",
    "    markup: True\n",
    "```\n",
    "\n",
    "Kivy的BBCode需要将markup属性设置为True。\n",
    ">如果要整行加粗，可以直接设bold属性为True。其他斜体、颜色、字体、大小同理。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###改变背景色"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "下面我们来调整窗口背景色，是`Window`对象的一个属性。可以在`__name__ == '__main__'`后面增加代码："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "from kivy.core.window import Window\n",
    "from kivy.utils import get_color_from_hex\n",
    "\n",
    "Window.clearcolor = get_color_from_hex('#101216')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "函数`get_color_from_hex`允许使用[CSS的RGB颜色值(`#RRGGBB`)](https://\n",
    "developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_started/Color)，也可以用其他函数。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##显示时间"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "大多数UI框架都是事件驱动，Kivy也不例外。这种方式相比通常的程序更简单——事件驱动的代码需要不断返回到主循环（`main loop`）；但是，这么做不能处理用户行为（点击鼠标，改变窗口），而且界面会冻结（`freeze`），Windows经常这样`程序停止响应`。\n",
    "\n",
    "总之，不能在程序里面加无限循环实现。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "# Don't do this\n",
    "while True:\n",
    "    update_time() # some function that displays time\n",
    "    sleep(1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "理论上可行，但UI实际会失去相应，直到系统或用户关闭进程才结束。记住Kivy内部一直运行主循环，我们可以通过事件与计算器来利用它。\n",
    "\n",
    "事件驱动还意味着我们需要对不同事件作出响应，可能是用户输入，网络行为，或超时等等。\n",
    "\n",
    "很多程序监听共同事件之一就是`App.on_start`，定义在类里面，在app初始化的时候调用。另一个常见的是`on_press`，当用户点击，tap，或其他按钮操作时启用。\n",
    "\n",
    "通过时间和计时器，我们就可以用Kivy自带的Clock类实现想要的功能。两个方法：\n",
    "* `Clock.schedule_once`：在一段时间后运行一次\n",
    "* `Clock.schedule_interval`：周期性的运行\n",
    "\n",
    "> 和JavaScript中的`window.setTimeout`和`window.setInterval`类似。其实Kivy和JS很像，即使API完全不同。\n",
    "\n",
    "`Clock`所有的计时事件都是Kivy主循环的一部分。这种方法与线程不同，这样调用一个阻塞函数可能会阻止其他事件被及时唤醒。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "###更新屏幕上的时间"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "要接入显示时间的`Label`部件，需要给它一个`id`，通过`id`属性来获取部件，这和Web开发类似。\n",
    "\n",
    "```yaml\n",
    "# clock.kv\n",
    "Label:\n",
    "    id: time\n",
    "```\n",
    "之后就可以通过`root.ids.time`来接入`Label`部件了。这里`root`就是`BoxLayout`。\n",
    "\n",
    "给`ClockApp`类增加一个`update_time`方法来更新时间："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def update_time(self, nap):\n",
    "    self.root.ids.time.text = strftime('[b]%H[/b]:%M:%S')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "再增加一个调度功能，让程序更新后每秒更新一次："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def on_start(self):\n",
    "    Clock.schedule_interval(self.update_time, 1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "运行程序看看是不是开始更新了。代码如下："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "# %load main.py\n",
    "from kivy.app import App\n",
    "from kivy.clock import Clock\n",
    "from kivy.core.window import Window\n",
    "from kivy.utils import get_color_from_hex\n",
    "\n",
    "from time import strftime\n",
    "\n",
    "class ClockApp(App):\n",
    "\n",
    "    def update_time(self, nap):\n",
    "        self.root.ids.time.text = strftime('[b]%H[/b]:%M:%S')\n",
    "\n",
    "    def on_start(self):\n",
    "        Clock.schedule_interval(self.update_time, 1)\n",
    "\n",
    "if __name__ == '__main__':\n",
    "\n",
    "    Window.clearcolor = get_color_from_hex('#301216')\n",
    "    ClockApp().run()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "看看python的`time`标准库`strftime`函数是如何与Kivy的BBCode组合成C语言字符串的。\n",
    "\n",
    "```yaml\n",
    "# %load clock.kv\n",
    "BoxLayout:\n",
    "    orientation: 'vertical'\n",
    "\n",
    "    Label:\n",
    "        text: '[b]00[/b]:00:00'\n",
    "        markup: True\n",
    "        id: time\n",
    "```        "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "###用属性绑定部件"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "除了ID绑定部件，还可以新建一个属性，在kv文件中进行绑定。这么做更符合DRY原则，只是多几行代码。如下所示："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "# In main.py\n",
    "from kivy.properties import ObjectProperty\n",
    "from kivy.uix.boxlayout import BoxLayout\n",
    "\n",
    "class ClockLayout(BoxLayout):\n",
    "    time_prop = ObjectProperty(None)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们在这段代码用`BoxLayout`写了个新root部件类，它有一个自定义属性`time_prop`，将连接`Label`部件。\n",
    "\n",
    "在`clock.kv`文件里，我们把属性绑定`id`，自定义属性和默认属性语法一致：\n",
    "\n",
    "```yaml\n",
    "# %load clock.kv\n",
    "ClockLayout:\n",
    "    time_prop: time\n",
    "    \n",
    "    Label:\n",
    "        id: time\n",
    "```\n",
    "\n",
    "这样，Python代码不需要知道`id`就可以连接`Label`部件，用新属性`root.time_prop.text = \"demo\"`。\n",
    "\n",
    "这样做使代码的可移植性更好，消除了反射（refactor）时Python代码同步的问题。靠属性还是`root.ids`去连接Python代码这事儿，只是代码风格问题，不重要。后面还会介绍其他Kivy属性的用法，让数据绑定更容易。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##布局基础"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Kivy提供了一堆`Layout`类来布局。`Layout`又是`Widget`类的子类，是个容器类。每个布局都是影响其子类位置和尺寸。\n",
    "\n",
    "在这个app中，我们的UI很直接，不需要什么神奇，如下所示：\n",
    "\n",
    "![layout](kbpic/1.3layout.png \"layout\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "做这种界面就要`BoxLayout`，一种一维网格。在`clock.kv`里面已经有`BoxLayout`了，只有一个子部件。Kivy的布局默认充满屏幕，所以自动适应屏幕。\n",
    "\n",
    "如果增加一个`Layout`，就会分一半屏幕，`vertical`和`horizontal`决定分割的方向。\n",
    "\n",
    "我们这里就用`vertical`分三块，然后中间那块用`horizontal`分两块，Esay吧。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###完成布局"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "由于中间这块是按钮，不应该比时间还大，可以增加一个`height`属性，然后设置`size_hint`属性为`None`。`size_hint`属性是一个元组`(宽, 高)`，影响部件的宽和高。如果你想用绝对高度和宽度，就要设置`size_hint`属性为`None`，否则高度和宽度设置无效，部件会自动计算尺寸。代码如下：\n",
    "\n",
    "```yaml\n",
    "# %load clock.kv\n",
    "BoxLayout:\n",
    "    orientation: 'vertical'\n",
    "    Label:\n",
    "        id: time\n",
    "        text: '[b]00[/b]:00:00'\n",
    "        font_name: 'Roboto'\n",
    "        font_size: 60\n",
    "        markup: True\n",
    "    BoxLayout:\n",
    "        height: 90\n",
    "        orientation: 'horizontal'\n",
    "        padding: 20\n",
    "        spacing: 20\n",
    "        size_hint: (1, None)\n",
    "        Button:\n",
    "            text: 'Start'\n",
    "            font_name: 'Roboto'\n",
    "            font_size: 25\n",
    "            bold: True\n",
    "        Button:\n",
    "            text: 'Reset'\n",
    "            font_name: 'Roboto'\n",
    "            font_size: 25\n",
    "            bold: True\n",
    "    Label:\n",
    "        id: stopwatch\n",
    "        text: '00:00.[size=40]00[/size]'\n",
    "        font_name: 'Roboto'\n",
    "        font_size: 60\n",
    "        markup: True\n",
    "```\n",
    "运行代码，会发现按钮没有完全填充`BoxLayout`，因为用了`padding`和`spacing`属性，与CSS类似。`main.py`代码如下："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "# %load main.py\n",
    "from kivy.app import App\n",
    "from kivy.clock import Clock\n",
    "from kivy.core.window import Window\n",
    "from kivy.utils import get_color_from_hex\n",
    "from kivy.core.text import LabelBase\n",
    "\n",
    "from time import strftime\n",
    "\n",
    "LabelBase.register(name='Roboto',\n",
    "    fn_regular='Roboto-Thin.ttf',\n",
    "    fn_bold='Roboto-Medium.ttf')\n",
    "\n",
    "class ClockApp(App):\n",
    "\n",
    "    def update_time(self, nap):\n",
    "        self.root.ids.time.text = strftime('[b]%H[/b]:%M:%S')\n",
    "\n",
    "    def on_start(self):\n",
    "        Clock.schedule_interval(self.update_time, 1)\n",
    "\n",
    "\n",
    "if __name__ == '__main__':\n",
    "\n",
    "    Window.clearcolor = get_color_from_hex('#123456')\n",
    "    ClockApp().run()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###减少重复"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "之前的kv代码一堆重复，其实可以借助CSS的方法是代码更精炼，更DRY。在`BoxLayout`外面增加一个新定义：\n",
    "\n",
    "```yaml\n",
    "# %load clock.kv\n",
    "<Label>:\n",
    "    font_name: 'Roboto'\n",
    "    font_size: 60\n",
    "    markup: True\n",
    "```\n",
    "这是一个类，与CSS的selector类似。每个`Label`都会带`<Label>`类特性。\n",
    "\n",
    "这样就可以把`clock.kv`里面每个`Label`的`font_name`，`font_size`和`markup`属性都删掉了。如果想改变一个属性的值，就直接写上，会覆盖原来的值，与CSS完全一样。\n",
    "> 定义类并没有创造一个新部件，只是一个属性集合。增加一个定义类，如果不使用就不会改变app的布局。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "####命名类"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "前面kv代码里类的处理有点问题，类只能有一个名字叫`Label`。当我们要为同一种部件加不同的属性定义类时，可以自定义类。如果直接改写`Label`和`Button`这些标准类，之后再用到通过类部件时改前改后一堆麻烦。所幸，命名类可以解决这一问题，`RobotoButton`是一种`Button`：\n",
    "\n",
    "```yaml\n",
    "<RobotoButton@Button>:\n",
    "    font_name: 'Roboto'\n",
    "    font_size: 25\n",
    "    bold: True\n",
    "```\n",
    "\n",
    "`@`前面是新类的名称，后面是部件类型，本质是面向对象的子类`class RobotoButton(Button)`，在kv代码里使用时，可以直接用命名类代替原来的`Button`类：\n",
    "\n",
    "```yaml\n",
    "RobotoButton:\n",
    "    text: 'Start'\n",
    "```\n",
    "\n",
    "命名类可以精简代码，而且可以改良部件。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##按钮样式"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "UI设计的死角是可点击元素，像按钮之类，没有一个统一样式。Win8的Metro风格十分激进，点击部分完全是纯色矩形，很小甚至基本没图案。Apple使用弧度；还有一种使用圆角的趋势，尤其在CSS3风格里。轻微的阴影也开始使用。\n",
    "\n",
    "Kivy在这方面很灵活，不强制任何一种风格，而且提供一堆特性帮你实现任意风格。其中之一就是9-patch缩放功能。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###9-patch scaling"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "传统UI开发中，如果背景的大小不一样，一般需要为每种大小都制作一张图片，这在button中尤为明显。当然我们也可以一小块一小块水平重复的画，也可以垂直的话。在android中专门有一种叫9-patch图片（以9.png结尾）来解决背景大小不一样时，只用一张背景图片。无论横屏还是竖屏，高分辨率还是低分辨率，都能自动填充满，而且不失真。\n",
    "\n",
    "缩放算法的目的就是尽可能的适应不同场合的像素要求，尤其是包含一堆文字的按钮。等比放缩图片容易实现，但是由于变形比例问题，质量不太好。\n",
    "\n",
    "非等比的9-patch放大可以产生不失真的效果。其理念就是把图片分成若干静止的、可缩放的块。假设下图是个可缩放按钮。黄色部分是操作区，其他颜色都是边：\n",
    "\n",
    "![scalebutton](kbpic/1.4scalablebutton.png \"scalablebutton\")\n",
    "\n",
    "当红色区域被压缩时，蓝色区域大小不变。如下图所示：\n",
    "\n",
    "![stretchedbutton](kbpic/1.5stretchedbutton.png \"stretchedbutton\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "蓝色的角是不变的，红色的边可以垂直、水平缩放。图片中唯一等比变化的部分就是黄色的操作区，通常都是用纯色，也可以加上文字。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###使用9-patch图"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "本例中，我们用一个简单的1px边的纯色按钮，改下颜色就可以重用。如下所示：\n",
    "\n",
    "![1pxbutton](kbpic/1.61pxbutton.png \"1pxbutton\")\n",
    "\n",
    "按下去的状态就用相反的颜色，如下所示：\n",
    "\n",
    "![inversionbutton](kbpic/1.7inversionbutton.png \"inversionbutton\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "现在在`clock.kv`中添加9-patch图，我们需要告诉Kivy图像边的像素，因为默认是等比变化的。\n",
    "\n",
    "```yaml\n",
    "<RobotoButton@Button>:\n",
    "    background_normal: 'button_normal.png'\n",
    "    background_down: 'button_down.png'\n",
    "    border: (2, 2, 2, 2)\n",
    "```\n",
    "\n",
    "`border`属性与CSS一致是顺时针：上，右，下，左。不过，不能像CSS里面直接写统一值`border: 2`，暂时还不行。\n",
    "\n",
    "> 当然用Python语法`border: [2] * 4`是最短的。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "前面说过，与CSS类似，后面的属性会覆盖前面同名的属性，比如新建`Reset`按钮，就可以在`RobotoButton`下修改：\n",
    "\n",
    "```yaml\n",
    "RobotoButton:\n",
    "    text: 'Reset'\n",
    "    background_normal: 'red_button_normal.png'\n",
    "    background_down: 'red_button_down.png'\n",
    "```\n",
    "\n",
    "这样按钮就搞定了，但是还不能运行，下面我们来实现秒表功能。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##计时功能"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "秒表不只是显示时间，还需要暂停、复位，比普通的钟表要复杂一点。反映到程序上，就是Python的`datatime`模块和`strftime()`函数的区别。后者可以直接将现在的时间格式化，正是秒表显示所要的。\n",
    "\n",
    "首先，我们要建立一个计时器。由于Kivy的`Clock.schedule_interval `事件handler支持时间参数，所以不通过Python的时间函数也容易实现。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def on_start(self):\n",
    "    Clock.schedule_interval(self.update, 0.016)\n",
    "def update(self, nap):\n",
    "    pass "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "时间单位是秒，就是说app每秒运行60次(60fps)为1帧，平均间隔时间为\n",
    "$$\\frac{1}{60} = 0.016(6)$$\n",
    "\n",
    "然后就是时间持续更新："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "class ClockApp(App):\n",
    "    sw_seconds = 0\n",
    "    \n",
    "    def update(self, nap):\n",
    "        self.sw_seconds += nap "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们先做时间显示功能，然后再实现停止功能。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###秒表时间格式"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "对于主时间显示，格式很简单，因为标准模块`strftime`提供了`datetime`时间转换字符串的功能。但是这个函数有一些不足：\n",
    "\n",
    "* 只接受Python的`datetime`时间格式（但是秒表需要秒用小数显示`sw_seconds`）\n",
    "* 没有十进制秒的转换功能\n",
    "\n",
    "`datetime`的不足容易克服：可以将`sw_seconds`转换为`datetime`时间格式。但是有点多余，因为我们最后还是需要小数显示，所以`strftime`格式不行。那么我们就自己做个轮子。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###计算时间"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "首先计算分、秒和分秒，`divmode`函数输出(商，余数)。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "minutes, seconds = divmod(self.sw_seconds, 60) "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`divmode`函数只计算一次，普通`/`和`%`运算需要两次。如果我们每一帧画面都有大量这样的浮点数除法，就像游戏或仿真，CPU就费劲了。\n",
    "\n",
    ">不太同意所谓“过早优化是魔鬼”，许多差的实践导致程序性能低下，其实一开始很容易避免，而且不影响代码质量，不去做才是魔鬼。\n",
    "\n",
    "要注意`divmode`函数结果都还是浮点数，所以要去争：`int(minutes)`和 `int(seconds)`。\n",
    "\n",
    "现在就剩下分秒了，可以这样获得："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "int(seconds * 100 % 100) "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###实现秒表"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "现在所有的数值都有了，让我们组合一下。Python的字符串处理有很多格式，与The Zen of Python（打开Python输入import this）的 \"There should be one—and preferably only one—obvious way to do it\"并不一致，呵呵。最简单的就是%为代表的C语言风格。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def update_time(self, nap):\n",
    "    self.sw_seconds += nap\n",
    "    minutes, seconds = divmod(self.sw_seconds, 60)\n",
    "    self.root.ids.stopwatch.text = (\n",
    "        '%02d:%02d.[size=40]%02d[/size]' %\n",
    "        (int(minutes), int(seconds),\n",
    "        int(seconds * 100 % 100))) "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "现在有分秒了，之前用的更新频率1fps就不适用了。让我们把` update_time `时间间隔改为0，即每一帧都更新："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "Clock.schedule_interval(self.update_time, 0)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    ">目前，大多数显示都是60fps，我们的值精确到1/100s，1秒钟100次更新。但是这么做没啥意义，因为在普通硬件上，人不会识别出100fps和60fps的区别。\n",
    "\n",
    ">就是说，很多时候你的代码应该与帧率分离，因为它的效果依赖于用户的硬件，硬件种类千差万别，没法儿预测你的app会down在什么机子上。\n",
    "\n",
    "运行程序会看到时间更新，但是还缺少控件，下面就是。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##秒表控件"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "用按钮来控制应用是最简单的。下面就是所有代码："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def start_stop(self):\n",
    "    self.root.ids.start_stop.text = ('Start'\n",
    "        if self.sw_started else 'Stop')\n",
    "    self.sw_started = not self.sw_started\n",
    "\n",
    "def reset(self):\n",
    "    if self.sw_started:\n",
    "        self.root.ids.start_stop.text = 'Start'\n",
    "        self.sw_started = False\n",
    "    self.sw_seconds = 0 "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "第一个事件handler是**Start**和**Stop**按钮，由`sw_started`改变状态实现。第二个handler是**Reset**按钮。\n",
    "\n",
    "还需要增加状态属性跟踪秒表是否在运行："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "class ClockApp(App):\n",
    "    sw_started = False\n",
    "    sw_seconds = 0\n",
    "    \n",
    "    def update_clock(self, nap):\n",
    "        if self.sw_started:\n",
    "            self.sw_seconds += nap"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们改变`update_clock`函数只有秒表开始`sw_started`为`True`才更新，秒表开始默认为停止状态。\n",
    "\n",
    "在`clock.kv`文件里，我们把方法绑定到`on_press`事件上：\n",
    "\n",
    "```yaml\n",
    "RobotoButton:\n",
    "    id: start_stop\n",
    "    text: 'Start'\n",
    "    on_press: app.start_stop()\n",
    "\n",
    "RobotoButton:\n",
    "    id: reset\n",
    "    text: 'Reset'\n",
    "    on_press: app.reset()\n",
    "```\n",
    ">在Kivy语言里面，有几个上下文相关的参考：\n",
    "> * self：引用当前部件；\n",
    "> * root：整个程序中最外层的部件；\n",
    "> *  app：应用类的一个实例。\n",
    "\n",
    "你会发觉，按钮事件处理一点也不难。就这样，我们的app实现了秒表的交互功能，允许用户开始，停止，复位。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##总结"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "这一章我们做了一个app，如果要打包并发布到Google Play或其他商店供大家用，还需要一点工作，因为涉及到具体的平台，但是最难的部分——编程——已经结束。\n",
    "\n",
    "通过个app，我们学习了ivy应用开发的很多方面，并不需要太多复杂代码就搞定了。Kivy的主要特点就是短小精悍的代码，允许快速迭代。一点点旧代码就可以获得很多新特性。Kivy生命力旺盛，将长盛不衰。\n",
    "\n",
    "这本书所以内容的共同基础是，无论我们的程序还是Kivy，都不是凭空产生的。一切都源自Python的`cheese shop`——[Python Package Index (PyPI)](http://pypi.python.org)——以及其他工具包，包括操作系统底层服务。\n",
    "\n",
    "我们还更新了许多网页应用开发的资源，如CSS框架Bootstrap中的字体、颜色和阴影。当然也希望你看看Google的`Material design principles`——不仅只是设计资源集合，也是一个完整教程，教我们实现风格统一、界面友好的UI，同时保留app的\"个性\"和特点。\n",
    "\n",
    "当然，这才刚刚开始。欲知后事如何，请听下回分解。"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.5.1"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 0
}
